# encoding: utf-8

import woo
import woo.core
import sys
import codecs
import re
from minieigen import *
import logging


sphinxOnlineDocPath='http://www.woodem.org/'
"Base URL for the documentation. Packaged versions should change to the local installation directory."

import os.path
# find if we have docs installed locally from package
sphinxLocalDocPath=woo.config.prefix+'/share/doc/woo'+woo.config.suffix+'/html/'
# FIXME: unfortunately file:// links don't seem to work with anchors, so just make this invalid
sphinxBuildDocPath=woo.config.sourceRoot+'/doc/sphinx2/build/html/REMOVE_THIS_TO_USE_LOCAL_DOCS'
# we prefer the packaged documentation for this version, if installed
if   os.path.exists(sphinxLocalDocPath+'/index.html'): sphinxPrefix='file://'+sphinxLocalDocPath
# otherwise look for documentation generated in the source tree
elif  os.path.exists(sphinxBuildDocPath+'/index.html'): sphinxPrefix='file://'+sphinxBuildDocPath
# fallback to online docs
else: sphinxPrefix=sphinxOnlineDocPath


def fixDocstring(s):
    if sys.version_info[0]==2 and isinstance(s,str): s=s.decode('utf-8')
    s=s.replace(':ref:',':obj:')
    s=re.sub(r'(?<!\\)\$([^\$]+)(?<!\\)\$',r'\ :math:`\1`\ ',s)
    s=re.sub(r'\\\$',r'$',s)
    if 'Overloaded function.' in s:
        print(100*'#\n\n'+s)
    return s

allWooClasses,allWooMods=set(),set()

def _ensureInitialized():
    'Fill allWooClasses and allWooMods, called automatically as needed'
    global allWooClasses,allWooMods
    if allWooClasses: return  # do nothing if already filled
    def subImport(pkg,exclude=None):
        'Import recursively all subpackages'
        import pkgutil
        for importer, modname, ispkg in pkgutil.iter_modules(pkg.__path__):
            fqmodname=pkg.__name__+'.'+modname
            print(fqmodname)
            if exclude and re.match(exclude,fqmodname):
                print('Skipping  '+fqmodname)
                continue
            if fqmodname not in sys.modules:
                sys.stdout.write('Importing '+fqmodname+'... ')
                try:
                    __import__(fqmodname,pkg.__name__)
                    print('ok')
                except (ImportError, NameError): print('(error, ignoring)')
            if fqmodname in sys.modules and ispkg:
                print('Import submodules from ',fqmodname)
                subImport(sys.modules[fqmodname])
                
    modExcludeRegex=r'^woo\.(_cxxInternal.*)(\..*|$)'
    subImport(woo,exclude=modExcludeRegex)
    try:
        import wooExtra
        subImport(wooExtra)
    except ImportError:
        print('No wooExtra modules imported.')

    for m in woo.master.compiledPyModules:
        if m not in sys.modules:
            print('Importing',m)
            __import__(m)

    # global allWooClasses, allWooMods
    cc=woo.system.childClasses(woo.core.Object,includeBase=True,recurse=True)
    for c in cc:
        if re.match(modExcludeRegex,c.__module__): continue
        if c.__doc__: c.__doc__=fixDocstring(c.__doc__)
        allWooClasses.add(c)

    allWooMods=set([sys.modules[m] for m in sys.modules if m.startswith('woo') and sys.modules[m] and sys.modules[m].__name__==m])
    

def allWooPackages(outDir='/tmp',skip=r'^(woo|wooExtra(|\..*))$'):
    '''Generate documentation of packages in the Restructured Text format. Each package is written to file called *out*/.`woo.[package].rst` and list of files created is returned.'''

    global allWooClasses,allWooMods
    _ensureInitialized()
    
    modsElsewhere=set()
    for m in allWooMods: modsElsewhere|=set(tuple(m._docInlineModules) if hasattr(m,'_docInlineModules') else ())

    # woo.foo.* modules go insides woo.foo
    #print(repr(modsElsewhere))
    toplevMods=set([m for m in allWooMods if (m not in modsElsewhere) and len(m.__name__.split('.'))<3])
    rsts=[]
    print('TOPLEVEL MODULES',[m.__name__ for m in toplevMods])
    print('MODULES DOCUMENTED ELSEWHERE',[m.__name__ for m in modsElsewhere])

    for mod in toplevMods:
        if re.match(skip,mod.__name__):
            print('[SKIPPING %s]'%(mod.__name__))
            continue
        outFile=outDir+'/%s.rst'%(mod.__name__)
        print('WRITING',outFile,mod.__name__)
        rsts.append(outFile)
        out=codecs.open(outFile,'w','utf-8')
        oneModuleWithSubmodules(mod,out)

    return rsts

def guessInstanceTypeFromCxxType(klass,trait,noneOnFail=False):
    import logging
    'Return type object guessed from cxxType'
    wHead=klass.__module__+'.'+klass.__name__+'.'+trait.name
    m=re.match(r'^\s*(weak_ptr\s*<|shared_ptr\s*<)?([A-Za-z0-9_:]+)(\s*>)?\s*',trait.cxxType)
    if m:
        cT=m.group(2)
        logging.debug('%s: got c++ base type: %s -> %s'%(wHead,trait.cxxType,cT))
        klasses=[c for c in woo.system.childClasses(woo.core.Object,includeBase=True) if c.__name__==cT]
        if len(klasses)==0:
            if noneOnFail: return None
            logging.warn('%s: no Python type object with name %s found (cxxType=%s)'%(wHead,cT,trait.cxxType))
        elif len(klasses)>1: logging.warn('%s: multiple Python types with name %s found (cxxType=%s): %s'%(wHead,cT,trait.cxxType,', '.join([c.__module__+'.'+c.__name__ for c in klasses])))
        else: return klasses[0]
    if noneOnFail: return None
    logging.warn('%s: no c++ base type found for %s'%(wHead,trait.cxxType))
    logging.warn('%s: using woo.core.Object as type'%(wHead))
    return woo.core.Object

def guessListTypeFromCxxType(klass,trait,warnFail=False):
    "Guess type of array from parsing trait.cxxType. Ugly but works."
    # head for warnings
    wHead=klass.__module__+'.'+klass.__name__+'.'+trait.name
    def vecTest(T,cxxT):
        regexp=r'^\s*(std\s*::)?\s*vector\s*<\s*(shared_ptr\s*<\s*)?\s*(std\s*::)?\s*('+T+r')(\s*>)?\s*>\s*$'
        m=re.match(regexp,cxxT)
        return m
    def vecGuess(T):
        regexp=r'^\s*(std\s*::)?\s*vector\s*<\s*(shared_ptr\s*<\s*)?\s*(std\s*::)?\s*(?P<elemT>[a-zA-Z_][a-zA-Z0-9_]+)(\s*>)?\s*>\s*$'
        m=re.match(regexp,T)
        return m
    from woo import dem
    if 'opengl' in woo.config.features: from woo import gl
    from woo import core
    vecMap={
        'bool':bool,'int':int,'long':int,'Particle::id_t':int,'size_t':int,
        'Real':float,'float':float,'double':float,
        'Vector6r':Vector6,'Vector6i':Vector6i,'Vector3i':Vector3i,'Vector2r':Vector2,'Vector2i':Vector2i,
        'Vector3r':Vector3,'Matrix3r':Matrix3,'Quaternionr':Quaternion,
        'VectorXr':VectorX,'MatrixXr':MatrixX,
        'AlignedBox2r':AlignedBox2,'AlignedBox3r':AlignedBox3,
        'string':str
    }
    cxxT=trait.cxxType
    if not cxxT:
        logging.error("Trait for %s does not define cxxType"%(trait.name))
        return None
    for T,ret in vecMap.items():
        if vecTest(T,cxxT):
            logging.debug("Got type %s from cxx type %s"%(repr(ret),cxxT))
            return (ret,)
    #print 'No luck with ',T
    m=vecGuess(cxxT)
    if m:
        # print 'guessed literal type',m.group('elemT')
        elemT=m.group('elemT')
        klasses=[c for c in woo.system.childClasses(woo.core.Object,includeBase=True) if c.__name__==elemT]
        if len(klasses)==0: logging.warn('%s: no Python type object with name %s found (cxxType=%s)'%(wHead,elemT,trait.cxxType))
        elif len(klasses)>1: logging.warn('%s: multiple Python types with name %s found (cxxType=%s): %s'%(wHead,elemT,trait.cxxType,', '.join([c.__module__+'.'+c.__name__ for c in klasses])))
        else: return (klasses[0],) # return tuple to signify sequence
    if warnFail: logging.error("Unable to guess python type from cxx type '%s'"%cxxT)
    return None


def makeTraitInfo(obj,klass,trait):
    def maybe_decode(a): return (a if isinstance(a,str) else str(a,'utf-8'))

    hasVal=True # true if the value is accessible from python
    try: val=getattr(obj,trait.name)
    except: hasVal=False

    ret=[]
    if trait.static: ret.append('static')
    tt=None
    if hasattr(trait,'pyType'):
        if isinstance(trait.pyType,(list,tuple)): tt='['+trait.pyType[0].__name__+", …]"
        elif trait.pyType is not None: tt=trait.pyType.__name__
        # else: ret.append('type: None?') # ??
    if tt is None:
        if hasVal and not isinstance(val,woo.core.Object) and val!=None:
            if val.__class__.__module__=='minieigen': tt=':obj:`%s <minieigen:minieigen.%s>`'%(val.__class__.__name__,val.__class__.__name__)
            else: tt=trait.cxxType
        if not tt:
            if trait.cxxType in ('int','string','bool','Real','long','size_t','ContainerT','PendingContact','std::vector<PendingContact>','list<id_t>','shared_ptr<SpherePack>','list<id_t>','boost_multi_array_real_5','py::object'): tt=trait.cxxType
        if not tt:
            l=guessListTypeFromCxxType(klass,trait,warnFail=False)
            if l and l[0].__module__!='__builtin__': tt=trait.cxxType.replace(l[0].__name__,':obj:`%s <%s.%s>`'%(l[0].__name__,l[0].__module__,l[0].__name__))
        if not tt:
            t=guessInstanceTypeFromCxxType(klass,trait,noneOnFail=True)
            if t and t.__module__!='__builtin__': tt=trait.cxxType.replace(t.__name__,':obj:`%s <%s.%s>`'%(t.__name__,t.__module__,t.__name__))
        if not tt: tt=trait.cxxType
    ret.append('type: '+tt)
    if trait.unit:
        if len(trait.unit)==1: ret.append('unit: '+maybe_decode(trait.unit[0]))
        else: ret.append(u'units: ['+u','.join(maybe_decode(u) for u in trait.unit)+u']')
    if trait.prefUnit and trait.unit[0]!=trait.prefUnit[0][0]:
        if len(trait.unit)==1: ret.append('preferred unit: '+maybe_decode(trait.prefUnit[0][0]))
        else: ret.append('preferred units: ['+u','.join(maybe_decode(u[0]) for u in trait.prefUnit)+']')
    if trait.range: ret.append(u'range: %g−%g'%(trait.range[0],trait.range[1]))
    if trait.noGui: ret.append('not shown in the UI')
    if trait.noDump: ret.append('not dumped')
    if trait.noSave: ret.append('not saved')
    if trait.hidden: ret.append('not accessible from python')
    if trait.readonly: ret.append('read-only in python')
    if trait.deprecated: ret.append('**DEPRECATED**, raises ``ValueError`` when accessed')
    if trait.choice and not trait.namedEnum: # named enums formatted differently below
        if isinstance(trait.choice[0],tuple): ret.append('choices: '+', '.join('%d = %s'%(c[0],c[1] if '|' not in c[1] else '``'+c[1]+'``') for c in trait.choice))
        else: ret.append('choices: '+', '.join(str(c) for c in trait.choice))
    if trait.namedEnum: ret.append('named enum, possible values are: '+trait.namedEnum_validValues(pre0="**'",post0="'**",pre="*'",post="'*"))
    if trait.filename: ret.append('filename')
    if trait.existingFilename: ret.append('existing filename')
    if trait.dirname: ret.append('directory name')
    if trait.bits: ret.append('bit accessors: '+', '.join(['**'+b+'**' for  b in trait.bits]))

    return ', '.join(ret)
    
def classSrcHyperlink(klass):
    'Return ReST-formatted line with hyperlinks to class headers (and implementation, if the corresponding .cpp file exists).'
    def findFirstLine(ff,pattern,regex=True):
        if regex: pat=re.compile(pattern)
        if not os.path.exists(ff): return -1
        numLines=[i for i,l in enumerate(open(ff).readlines()) if (pat.match(l) if regex else (l==pattern))]
        return (numLines[0] if numLines else -1)

    import woo.config, inspect, os, hashlib
    pysrc=inspect.getsourcefile(klass)
    if pysrc: # python class
        lineNo=inspect.getsourcelines(klass)[1]
        pysrc1=os.path.basename(pysrc)
        # locate the file in the source tree (as a quick hack, just use filename match)
        matches=[]
        for root,dirnames,filenames in os.walk(woo.config.sourceRoot):
            relRoot=root[len(woo.config.sourceRoot)+1:]
            if relRoot.startswith('build') or relRoot.startswith('debian'): continue # garbage
            matches+=[relRoot+'/'+f for f in filenames if f==pysrc1]
        if len(matches)>1:
            #print 'WARN: multiple files named %s, using the first one: %s'%(pysrc1,str(matches))
            def fileHash(src):
                if sys.version_info[0]==2: return hashlib.md5(open(src).read()).hexdigest()
                else: return hashlib.md5(open(src).read().encode('utf-8')).hexdigest()
            md0=fileHash(pysrc)
            matches=[m for m in matches if fileHash(woo.config.sourceRoot+'/'+m)==md0]
            if len(matches)==0:
                print('WARN: no file named %s with matching md5 digest found in source tree?'%pysrc1)
                return None
            elif len(matches)>1:
                print('WARN: multiple files named %s with the same md5 found in the source tree, using the one with shortest path.'%pysrc1)
                matches.sort(key=len)
                match=matches[0]
            else: match=matches[0]
        elif len(matches)==1: match=matches[0]
        else:
            print('WARN: no python source %s found.'%(pysrc1))
            return None
        return '[:woosrc:`%s <%s#L%s>`]'%(match,match,lineNo)


        
    if not klass._classTrait: return None
    # woo.config.buildRoot
    f=os.path.abspath(klass._classTrait.file)
    src=os.path.abspath(woo.config.sourceRoot)
    if not f.startswith(src): print('Absolute source path %s does not start with absolute source dir %s!'%(f,src))
    f2=f[len(src)] 
    if 0:
        if f.startswith(woo.config.sourceRoot): commonRoot=woo.config.sourceRoot
        elif f.startswith(woo.config.buildRoot): commonRoot=woo.config.buildRoot
        elif not f.startswith('/'): commonRoot=''
        else:
            print('File where class is defined (%s) does not start with source root (%s) or build root (%s)'%(f,woo.config.sourceRoot,woo.config.buildRoot))
            return None
        # +1 removes underscore in woo/...
    # if this header was copied into include/, get rid of that now
    m=re.match('include/woo/(.*)',f2)
    if m: f2=m.group(1)
    if f2.endswith('.hpp'): hpp,cpp,lineIs=f2,f2[:-4]+'.cpp','hpp'
    elif f2.endswith('.cpp'): hpp,cpp,lineIs=f2[:-4]+'.hpp',f2,'cpp'
    else: return None # should not happen, anyway

    hppLine=findFirstLine(woo.config.sourceRoot+'/'+hpp,r'^(.*\s)?(struct|class)\s+%s\s*({|:).*$'%klass.__name__)
    cppLine=findFirstLine(woo.config.sourceRoot+'/'+cpp,r'^(.*\s)?%s::.*$'%klass.__name__)
    ret=[]
    if hppLine>0: ret+=[':woosrc:`%s <%s#L%d>`'%(hpp,hpp,hppLine+1)]
    else:
        if f2.endswith('.hpp'): ret+=[':woosrc:`%s <%s#L%d>`'%(hpp,hpp,klass._classTrait.line)]
        else: print('WARN: No header line found for %s in %s'%(klass.__name__,hpp))
    if cppLine>0: ret+=[':woosrc:`%s <%s#L%d>`'%(cpp,cpp,cppLine+1)]
    else: pass # print 'No impl. line found for %s in %s'%(klass.__name__,cpp)
    #ret=[':woosrc:`header <%s#L%d>`'%(f2,klass._classTrait.line)]
    #if f2.endswith('.hpp'):
    #    cpp=woo.config.sourceRoot+'/'+f2[:-4]+'.cpp'
    #    # print 'Trying ',cpp
    #    if os.path.exists(cpp):
    #        # find first line in the cpp file which contains KlassName:: -- that's most likely where the implementation begins
    #        numLines=[i for i,l in enumerate(open(cpp).readlines()) if klass.__name__+'::' in l]
    #        if numLines: ret.append(':woosrc:`implementation <%s#L%d>`'%(f2[:-4]+'.cpp',numLines[0]+1))
    #        # return just the cpp file if the implementation is probably not there at all?
    #        # don't do it for now
    #        # else: ret.append(':woosrc:`implementation <%s#L>`'%(f2[:-4]+'.cpp'))
    if ret: return '[ '+' , '.join(ret)+' ]'
    else: return None

def classDocHierarchy_topsAndDict(mod):
    'Return tuple containing list of top-level class objects, and dictionary which maps all module-contained class objects to classes which should be documented under it (derived classes, and doc-related classes specified via ClassTrait.section'
    global allWooClasses,allWooMods
    _ensureInitialized()
    # kk=woo.system.childClasses(woo.core.Object,includeBase=True) # all woo classes
    dd={}
    for k in allWooClasses:
        if k==woo.core.Object: continue # obejct itself is not documented this way
        t=k._classTrait
        if not t: continue
        if k.__module__!=mod.__name__: continue
        dd[k]=[]
        for a in t.docOther:
            if '.' in a:
                raise NotImplementedError("Cross-module documentation not yet supported.")
                dd[k].append(eval(a))
            else: dd[k].append(eval(k.__module__+'.'+a))
            # [eval('%s.%s'%(k.__module__,a)) for a in t.docOther] # perhaps not in the same module? fix here!!
        #print t.name,t.docOther,t.intro,t.title

    for k in dd:
        # if the base is documented, put it under the base class (prepend)
        b=k.__bases__[0]
        if b in dd: dd[b]=[k]+dd[b]
    tops=[]
    for k in dd:
        refs=[]
        for kk,vv in dd.items():
            if k in vv: refs+=[kk]
        if len(refs)==0: tops.append(k)
        elif len(refs)>1: raise RuntimeError('Class %s listed under multiple classes: '%k.__name__+', '.join(['%s'%(r.__name__) for r in refs]))
        # if the class has one single reference, it is not a top class
    return (tops,dd)


def oneModuleWithSubmodules(mod,out,exclude=None,level=0,importedInto=None):
    global allWooClasses,allWooMods
    _ensureInitialized()
    if exclude==None: exclude=set() # avoid referenced empty set to be modified
    if level>=0: out.write(':mod:`%s`\n%s\n\n'%(mod.__name__,(20+len(mod.__name__))*('=-^"'[level])))
    if importedInto:
        out.write('.. note:: This module is imported into the :obj:`%s` module automatically; refer to its objects through :obj:`%s`.\n\n'%(importedInto.__name__,importedInto.__name__))

    def _docOneClass(k):
        if k in exclude: return
        exclude.add(k) # already-documented should not be documented again
        obj=k()

        kOut.write('.. autoclass:: %s\n'%k.__name__)
        #kOut.write('   :members: %s\n'%(','.join([m for m in dir(k) if (not m.startswith('_') and m not in set(trait.name for trait in k._attrTraits))])))
        kOut.write('   :members:\n')
        # those will be documented explicitly
        ex=[t.name for t in k._attrTraits]
        if issubclass(k,woo.core.Object):
            # this does not seem to work really...
            # exclude __init__ which would be shown by special-members, but is really useless for Object (always the same)
            ex.append('__init__')
            ex.append('__getstate__')
            ex.append('__setstate__')
        if ex: kOut.write('   :exclude-members: %s\n'%(', '.join(ex)))
        kOut.write('   :special-members:\n')

        kOut.write('\n')

        srcXref=classSrcHyperlink(k)
        if srcXref: kOut.write('\n   '+srcXref+'\n\n')
        for trait in k._attrTraits:
            try:
                iniStr=' (= %s)'%(repr(trait.ini))
            except TypeError: # no converter found
                iniStr=''
            if trait.startGroup:
                kOut.write(u'   .. rubric:: ► %s\n\n'%(trait.startGroup))
            kOut.write('   .. attribute:: %s%s\n\n'%(trait.name,iniStr))
            for l in fixDocstring(trait.doc).split('\n'): kOut.write('      '+l+'\n')
            traitInfo=makeTraitInfo(obj,k,trait)
            if traitInfo: kOut.write(u'\n      ['+traitInfo+']\n')
            kOut.write('\n')

    klasses=[c for c in allWooClasses if (c.__module__==mod.__name__ or (hasattr(mod,'_docInlineModules') and c.__module__ in sys.modules and sys.modules[c.__module__] in mod._docInlineModules))]
    klasses.sort(key=lambda x: x.__name__)
    #
    tops,klassesUnder=classDocHierarchy_topsAndDict(mod)

    #global prevLevel
    #prevLevel=0

    def _inheritanceDiagram(k,currmod):
        def nodeName(kk): return ('' if kk.__module__==currmod else kk.__module__+'.')+kk.__name__
        def mkNode(kk,style='solid',fillcolor=None): return '\t\t"%s" [shape="box",fontsize=8,style="setlinewidth(0.5),%s",%sheight=0.2,URL="%s.html#%s.%s"];\n'%(nodeName(kk),style,'fillcolor=%s,'%fillcolor if fillcolor else '','.'.join(kk.__module__.split('.')[:2]),kk.__module__,k.__name__)
        ret=".. graphviz::\n\n\tdigraph %s {\n\t\trankdir=LR;\n\t\tmargin=.2;\n"%k.__name__
        ret+=mkNode(k)
        cc=woo.system.childClasses(k,includeBase=False)
        for c in cc:
            if c.__module__.startswith('wooExtra.'): continue
            # this class will have its own diagram for inheritance
            # if hasattr(c,'_classTrait') and c._classTrait.title and c._classTrait.klassesUnder:
            if c.__module__==currmod: ret+=mkNode(c)
            else: ret+=mkNode(c,style='filled',fillcolor='grey')
            ret+='\t\t"%s" -> "%s" [arrowsize=0.5,style="setlinewidth(0.5)"]'%(nodeName(c.__bases__[0]),nodeName(c))
        return ret+'\n\t}\n\n'


    def _docOneClassWithSectioning(k,level=0):
        # print level*'\t',k.__name__
        if not hasattr(k,'_classTrait'): return # will be documented later
        t=k._classTrait
        nextLevel=level
        #global prevLevel
        #if not t.title:
        #    # decrasing level without section -- write dividing line
        #    if prevLevel>level: out.write('\n\n-----\n\n\n') #
        #prevLevel=level
        #if t.title: # write section title
        nextLevel=level+1
        tt=t.title if t.title else k.__name__
        if level<3:
            kOut.write('\n.. rst-class:: html-toggle\n\n')
            kOut.write('\n.. rst-class:: emphasized\n\n')
        kOut.write(tt+'\n'+len(tt)*'-+"%\'^_'[level]+'\n\n')
        #else:
        #    nextLevel=level+1
        #    kOut.write(k.__name__+'\n'+len(k.__name__)*('-"\'^+_%'[level])+'\n\n')
        if t.intro:
            kOut.write(t.intro+'\n\n')
            if not t.title: warnings.warn('Class %s.%s has intro but no title in class trait.'%(k.__module__,k.__name__))
        # if there was a title different from the class title and there are classes under us, repeat the class name here on the subordinate level
        if t.title:
            if nextLevel<3:
                kOut.write('\n.. rst-class:: html-toggle\n\n')
                kOut.write('\n.. rst-class:: emphasized\n\n')
            kOut.write(k.__name__+'\n'+len(k.__name__)*'-+"%\'^_'[nextLevel]+'\n\n')
        # print inheritance
        if k!=woo.core.Object:
            bb=[k]
            while True:
                # if len(bb[-1].__bases)==0: break
                bb+=[bb[-1].__bases__[0]]
                if bb[-1]==woo.core.Object: break
                if bb[-1]==object: break ## in one case this breaks...!??
            kOut.write(u'\n'+u' → '.join([u':obj:`~%s.%s`'%(b.__module__,b.__name__) for b in reversed(bb)])+'\n\n')
        # print derived classes
        if klassesUnder and woo.system.childClasses(k):
            kOut.write(_inheritanceDiagram(k,mod.__name__))
        _docOneClass(k)
        for kk in klassesUnder[k]:
            assert k!=kk
            # print kk.__name__
            _docOneClassWithSectioning(kk,nextLevel)
        #if not t.title and t.intro:

    # document any c++ classes in a special way
    # defer writing that to out though so that automodule can exclude classes which are documented manually
    import io
    kOut=io.StringIO()
    for top in tops: _docOneClassWithSectioning(top,level)

    # document all remaining classes linearly here, if any
    for k in klasses: _docOneClass(k)

    # control whether autodocs are at the bottom or at the top
    def writeAutoMod(mm,skip=None):
        out.write('.. automodule:: %s\n   :members:\n   :undoc-members:\n'%(mm.__name__))
        if skip: out.write('   :exclude-members: %s\n'%(',  '.join([e.__name__ for e in skip])))
        out.write('\n')
    autoAtBottom=False
    if autoAtBottom:
        # HACK: we want module's docstring to appear at the top, but autodocumented stuff at the bottom
        # dump __doc__ straight to the output here, and reset it, so that automodule does not pick it up
        # autodoc however automatically creates index entry for the module;
        # that's why we end up with bunch of SEVERE: Duplicate ID: "module-..." msgs
        out.write('.. module:: %s\n\n'%mod.__name__)
        if mod.__doc__:
            out.write(mod.__doc__+'\n\n')
            mod.__doc__=None
        # if there are no classes, avoid warning from sphinx
        if klasses: out.write('.. inheritance-diagram:: %s\n\n'%mod.__name__) 
        # insert documentation of classes
        out.write(kOut.getvalue())
        # document the rest of the module here (don't recurse)
        writeAutoMod(mod,skip=exclude)
    else:
        # automodule first, including inheritance tree (are excluded classes excluded from the tree as well?)
        # exlude classes which are are then documented manually
        # if there are no classes, avoid warning from sphinx
        if klasses: out.write('.. inheritance-diagram:: %s\n   :parts: %s\n\n'%(mod.__name__,len(mod.__name__.split('.')) if mod.__name__.startswith('woo.') else 0)) # without showing module name in the inheritance diagram
        writeAutoMod(mod,skip=exclude)
        out.write(kOut.getvalue())
    # imported modules
    if hasattr(mod,'_docInlineModules'):
        # negative level will skip heading
        for m in mod._docInlineModules: oneModuleWithSubmodules(m,out,exclude=exclude,level=level+1,importedInto=mod)
    # nested modules
    # with nested heading
    for m in [m for m in allWooMods if m.__name__.startswith(mod.__name__+'.') and len(mod.__name__.split('.'))+1==len(m.__name__.split('.'))]: oneModuleWithSubmodules(m,out,exclude=exclude,level=level+1)



def makeSphinxHtml(k):
    'Given a class, try to guess name of the HTML page where it is documented by Sphinx'
    if not k.__module__.startswith('woo.'): return k.__module__
    mod=k.__module__.split('.')[1] # sphinx does not make the hierarchy any deeper than 2
    for start,repl in [('_pack','pack'),('_qt','qt'),('_utils','utils')]:
        if mod.startswith(start): return 'woo.'+repl
    return 'woo.'+mod

def makeClassAttrDocUrl(klass,attr=None):
    '''Return URL to documentation of Woo class or its attribute in http://woodem.org.
    :param klass: class object
    :param attr:  attribute to link to. If given, must exist directly in given *klass* (not its parent); if not given or empty, link to the class itself is created and *attr* is ignored.
    :return: URL as text
    '''
    dotAttr=(('.'+attr) if attr else '')
    if klass.__module__.startswith('wooExtra.'):
        KEY=sys.modules['.'.join(klass.__module__.split('.')[:2])].KEY
        return 'https://www.woodem.eu/private/{KEY}/doc/index.html#{module}.{klass}{dotAttr}'.format(KEY=KEY,module=klass.__module__,klass=klass.__name__,dotAttr=dotAttr)
    return '{sphinxPrefix}/{sphinxHtml}.html#{module}.{klass}{dotAttr}'.format(sphinxPrefix=sphinxPrefix,sphinxHtml=makeSphinxHtml(klass),module=klass.__module__,klass=klass.__name__,dotAttr=dotAttr)

def makeObjectUrl(obj,attr=None):
    """Return HTML href to a *obj* optionally to the attribute *attr*.
    The class hierarchy is crawled upwards to find out in which parent class is *attr* defined,
    so that the href target is a valid link. In that case, only single inheritace is assumed and
    the first class from the top defining *attr* is used.

    :param obj: object of class deriving from :obj:`woo.core.Object`, or string; if string, *attr* must be empty.
    :param attr: name of the attribute to link to; if empty, linke to the class itself is created.

    :returns: HTML with the hyperref.
    """
    if attr:
        klass=obj.__class__
        while attr in dir(klass.__bases__[0]): klass=klass.__bases__[0]
    else:
        klass=obj.__class__
    return makeClassAttrDocUrl(klass,attr)

def makeObjectHref(obj,attr=None,text=None):
    '''Create HTML hyperlink, wrapping :obj:`makeObjectUrl`. adding ``<a href="...">text</a>``.

    :param text: visible text of the hyperlink; if not given, either class name or attribute name without class name (when *attr* is given) is used.
    '''
    if not text:
        if attr: text=attr
        else: text=obj.__class__.__name__
    return '<a href="%s">%s</a>'%(makeObjectUrl(obj,attr),text)


def makeCGeomFunctorsMatrix():
    import woo, woo.dem, woo.system
    import prettytable
    ggg0=woo.system.childClasses(woo.dem.CGeomFunctor)
    ggg=set()
    for g in ggg0:
        # handle derived classes which don't really work as functors (Cg2_Any_Any_L6Geom__Base)
        try: 
            g().bases
            ggg.add(g)
        except: pass 

    ss=list(woo.system.childClasses(woo.dem.Shape))
    ss.sort(key=lambda s: s.__name__)
    ss=[s for s in ss if s.__name__ not in ('Membrane','Tet4')] # Membrane is useless here, as it is the same as Facet

    def type2sphinx(t,name=None):
        if name==None: return ':obj:`~%s.%s`'%(t.__module__,t.__name__)
        else: return ':obj:`%s <%s.%s>`'%(name,t.__module__,t.__name__)

    t=prettytable.PrettyTable(['']+[type2sphinx(s) for s in ss],border=True,header=True,hrules=prettytable.ALL)
    for s1 in ss:
        row=[type2sphinx(s1)] # header column
        for s2 in ss:
            gg=[g for g in ggg if sorted(g().bases)==sorted([s1.__name__,s2.__name__])]
            cell=[]
            for g in gg:
                if g.__name__.endswith('L6Geom'): cell+=[type2sphinx(g,'l6g')]
                elif g.__name__.endswith('G3Geom'): cell+=[type2sphinx(g,'g3g')]
                else: raise RuntimError('CGeomFunctor name does not end in L6Geom or G3Geom.')
            cell.sort(reverse=True) # so that l6g comes first
            if cell: row.append(', '.join(cell))
            else: row.append(u'×')
        t.add_row(row)
    tt=t.get_string().split('\n')
    tt=[(tt[i] if i!=2 else tt[i].replace('-','=')) for i in range(len(tt))]
    # return '.. tabularcolumns:: |l|'+'|'.join(len(ss)*['c'])+'\n\n'
    return '\n'.join(tt)+'\n\n'


